// Point + text label rendering style

import log from '../../utils/log';
import {Style} from '../style';
import StyleParser from '../style_parser';
import gl from '../../gl/constants'; // web workers don't have access to GL context, so import all GL constants
import VertexLayout from '../../gl/vertex_layout';
import { buildQuadForPoint } from '../../builders/points';
import Texture from '../../gl/texture';
import Geo from '../../utils/geo';
import Collision from '../../labels/collision';
import LabelPoint from '../../labels/label_point';
import placePointsOnLine from '../../labels/point_placement';
import {TextLabels} from '../text/text_labels';
import {VIEW_PAN_SNAP_TIME} from '../../scene/view';
import debugSettings from '../../utils/debug_settings';

import points_vs from './points_vertex.glsl';
import points_fs from './points_fragment.glsl';

const PLACEMENT = LabelPoint.PLACEMENT;

export const Points = Object.create(Style);

const SHADER_POINT_VARIANT = '__shader_point';

// texture types
const TANGRAM_POINT_TYPE_TEXTURE = 1; // style texture/sprites (assigned by user)
const TANGRAM_POINT_TYPE_LABEL = 2;   // labels (generated by rendering labels to canvas)
const TANGRAM_POINT_TYPE_SHADER = 3;  // point (drawn in shader)

// default point size in pixels
const DEFAULT_POINT_SIZE = 16;

// Mixin text label methods
Object.assign(Points, TextLabels);

Object.assign(Points, {
    name: 'points',
    built_in: true,
    vertex_shader_src: points_vs,
    fragment_shader_src: points_fs,
    selection: true,  // enable feature selection
    collision: true,  // style includes a collision pass
    blend: 'overlay', // overlays drawn on top of all other styles, with blending

    init(options = {}) {
        Style.init.call(this, options);

        // Shader defines
        this.setupDefines();

        // Include code for SDF-drawn shader points
        this.defines.TANGRAM_HAS_SHADER_POINTS = true;

        // texture types
        this.defines.TANGRAM_POINT_TYPE_TEXTURE = TANGRAM_POINT_TYPE_TEXTURE;
        this.defines.TANGRAM_POINT_TYPE_LABEL = TANGRAM_POINT_TYPE_LABEL;
        this.defines.TANGRAM_POINT_TYPE_SHADER = TANGRAM_POINT_TYPE_SHADER;

        this.collision_group_points = this.name+'-points';
        this.collision_group_text = this.name+'-text';

        // Stenciling proxy tiles (to avoid compounding alpha artifacts) doesn't work well with
        // points/text labels, which have pure transparent pixels that interfere with the stencil buffer,
        // causing a "cut-out"/"x-ray" effect (preventing pixels that would usually be covered by proxy tiles
        // underneath from being rendered).
        this.stencil_proxy_tiles = false;

        this.reset();
    },

    // Setup defines common to points base and child (text) styles
    setupDefines () {
        // If we're not rendering as overlay, we need a layer attribute
        if (this.blend !== 'overlay') {
            this.defines.TANGRAM_LAYER_ORDER = true;
        }

        // Fade in labels
        if (debugSettings.suppress_label_fade_in === true) {
            this.fade_in_time = 0;
            this.defines.TANGRAM_FADE_IN_RATE = null;
        }
        else {
            this.fade_in_time = 0.15; // time in seconds
            this.defines.TANGRAM_FADE_IN_RATE = 1 / this.fade_in_time;
        }

        // Snap points to pixel grid after panning stop
        if (debugSettings.suppress_label_snap_animation !== true) {
            this.defines.TANGRAM_VIEW_PAN_SNAP_RATE = 1 / VIEW_PAN_SNAP_TIME; // inverse time in seconds
        }

        // Show hidden labels for debugging
        if (debugSettings.show_hidden_labels === true) {
            this.defines.TANGRAM_SHOW_HIDDEN_LABELS = true;
        }

        // Enable wireframe for debugging
        if (debugSettings.wireframe === true) {
            this.defines.TANGRAM_WIREFRAME = true;
        }
    },

    reset () {
        this.queues = {};
        this.resetText();
        this.texture_missing_sprites = {}; // track which missing sprites we've found (reduce dupe log messages)
    },

    // Override to queue features instead of processing immediately
    addFeature (feature, draw, context) {
        let tile = context.tile;
        if (tile.generation !== this.generation) {
            return;
        }

        // Point styling
        let style = {};
        style.color = this.parseColor(draw.color, context);
        style.texture = draw.texture;   // optional point texture, specified in `draw` or at style level
        style.label_texture = null;     // assigned by labelling code if needed
        style.blend_order = draw.blend_order; // copy pre-computed blend order

        // require color or texture
        if (!style.color && !style.texture) {
            return;
        }

        style.alpha = StyleParser.evalCachedProperty(draw.alpha, context); // optional alpha override

        // optional sprite and texture
        let sprite_info;
        if (this.hasSprites(style)) {
            // populate sprite_info object with used sprites
            sprite_info = this.parseSprite(style, draw, context);

            if (sprite_info) {
                style.texcoords = sprite_info.texcoords;
            }
            else {
                // sprites are defined in the style's texture, but none are used in the current layer
                log({ level: 'debug', once: true }, `Layer group '${draw.layers.join(', ')}' ` +
                    `uses a texture '${style.texture}', but doesn't specify which sprite to draw. ` +
                    'Features that match this layer group won\'t be drawn without specifying the sprite with the ' +
                    '\'sprite\' or \'sprite_default\' properties. The merged draw parameters for this layer group are:', draw).then(logged => {
                    if (logged) {
                        log('debug', `Example feature for layer group '${draw.layers.join(', ')}'`, feature);
                    }
                });
                return;
            }
        } else if (draw.sprite) {
            // sprite specified in the draw layer but no sprites defined in the texture
            log({ level: 'warn', once: true }, `Layer group '${draw.layers.join(', ')}' ` +
                `specifies sprite '${draw.sprite}', but the texture '${draw.texture}' doesn't define any sprites. ` +
                'Features that match this layer group won\'t be drawn. The merged draw parameters for this layer group are:', draw);
            return;
        }

        this.calcSize(draw, style, sprite_info, context);

        // incorporate outline into size
        if (draw.outline) {
            style.outline_width = StyleParser.evalCachedProperty(draw.outline.width, context) || StyleParser.defaults.outline.width;
            style.outline_color = this.parseColor(draw.outline.color, context);
        }

        style.outline_edge_pct = 0;
        if (style.outline_width && style.outline_color) {
            // adjust size and UVs for outline
            let outline_width = style.outline_width;
            style.size[0] += outline_width;
            style.size[1] += outline_width;
            style.outline_edge_pct = outline_width / Math.min(style.size[0], style.size[1]) * 2; // UV distance at which outline starts
            style.outline_alpha = StyleParser.evalCachedProperty(draw.outline.alpha, context); // optional alpha override
        }

        // size will be scaled to 16-bit signed int, so max allowed width + height of 256 pixels
        style.size[0] = Math.min(style.size[0], 256);
        style.size[1] = Math.min(style.size[1], 256);

        // Placement strategy
        style.placement = draw.placement;
        style.placement_min_length_ratio = StyleParser.evalCachedProperty(draw.placement_min_length_ratio, context);

        // Spacing parameter (in pixels) to equally space points along a line
        if (style.placement === PLACEMENT.SPACED && draw.placement_spacing) {
            style.placement_spacing = StyleParser.evalCachedProperty(draw.placement_spacing, context);
        }

        // Angle parameter (can be a number or the string "auto")
        style.angle = StyleParser.evalProperty(draw.angle, context) || 0;

        // points can be placed off the ground
        style.z = StyleParser.evalCachedDistanceProperty(draw.z, context) || StyleParser.defaults.z;

        style.tile_edges = draw.tile_edges; // usually activated for debugging, or rare visualization needs

        this.computeLayout(style, feature, draw, context, tile);

        // Text styling
        let tf =
            draw.text &&
            draw.text.visible !== false && // explicitly handle `visible` property for nested text
            this.parseTextFeature(feature, draw.text, context, tile);

        if (Array.isArray(tf)) {
            tf = null; // NB: boundary labels not supported for point label attachments, should log warning
            log({ level: 'warn', once: true }, `Layer group '${draw.layers.join(', ')}': ` +
                'cannot use boundary labels (e.g. \'text_source: { left: ..., right: ... }\') for \'text\' labels attached to \'points\'; ' +
                `provided 'text_source' value was ${JSON.stringify(draw.text.text_source)}`);
        }

        if (tf) {
            tf.layout.parent = style; // parent point will apply additional anchor/offset to text

            // Text labels have a default priority of 0.5 below their parent point (+0.5, priority is lower-is-better)
            // This can be overriden, as long as it is less than or equal to the default
            tf.layout.priority = draw.text.priority ? Math.max(tf.layout.priority, style.priority + 0.5) : (style.priority + 0.5);

            Collision.addStyle(this.collision_group_text, tile.id);
        }

        this.queueFeature({ feature, draw, context, style, text_feature: tf }, tile); // queue the feature for later processing

        // Register with collision manager
        Collision.addStyle(this.collision_group_points, tile.id);
    },

    // Calcuate the size for the current point feature
    calcSize (draw, style, sprite_info, context) {
        // point size defined explicitly, or defaults to sprite size, or generic fallback
        style.size = draw.size;
        if (!style.size) {
            // a 'size' property has not been set in the draw layer -
            // use the sprite size if it exists and a generic fallback if it doesn't
            style.size = (sprite_info && sprite_info.css_size) || [DEFAULT_POINT_SIZE, DEFAULT_POINT_SIZE];
        }
        else {
            // check for a cached size, passing the texture and any sprite references
            style.size = StyleParser.evalCachedPointSizeProperty(draw.size, sprite_info, Texture.textures[style.texture], context);
            if (style.size == null) {
                // the StyleParser couldn't evaluate a sprite size
                log({ level: 'warn', once: true }, `Layer group '${draw.layers.join(', ')}': ` +
                    `'size' (${JSON.stringify(draw.size.value)}) couldn't be interpreted, features that match ` +
                    'this layer group won\'t be drawn');
                return;
            }
            else if (typeof style.size === 'number') {
                style.size = [style.size, style.size]; // convert 1d size to 2d
            }
        }
    },

    hasSprites (style) {
        return style.texture && Texture.textures[style.texture] && Texture.textures[style.texture].sprites;
    },

    // Generate a sprite_info object
    getSpriteInfo (style, sprite) {
        let info = Texture.textures[style.texture].sprites[sprite] && Texture.getSpriteInfo(style.texture, sprite);
        if (sprite && !info) {
            // track missing sprites (per texture)
            this.texture_missing_sprites[style.texture] = this.texture_missing_sprites[style.texture] || {};
            if (!this.texture_missing_sprites[style.texture][sprite]) { // only log each missing sprite once
                log('debug', `Style: in style '${this.name}', could not find sprite '${sprite}' for texture '${style.texture}'`);
                this.texture_missing_sprites[style.texture][sprite] = true;
            }
        }
        else if (info) {
            info.sprite = sprite;
        }
        return info;
    },

    // Check a sprite name against available sprites and return a sprite_info object
    parseSprite (style, draw, context) {
        // check for functions
        let sprite = StyleParser.evalProperty(draw.sprite, context);
        let sprite_info = this.getSpriteInfo(style, sprite) || this.getSpriteInfo(style, draw.sprite_default);
        return sprite_info;
    },

    // Queue features for deferred processing (collect all features first so we can do collision on the whole group)
    queueFeature (q, tile) {
        if (!this.tile_data[tile.id] || !this.queues[tile.id]) {
            this.startData(tile);
        }
        this.queues[tile.id] = this.queues[tile.id] || [];
        this.queues[tile.id].push(q);
    },

    // Override
    async endData (tile) {
        if (tile.canceled) {
            log('trace', `Style ${this.name}: stop tile build because tile was canceled: ${tile.key}`);
            return null;
        }

        let queue = this.queues[tile.id];
        delete this.queues[tile.id];

        // For each point feature, create one or more labels
        let text_objs = [];
        let point_objs = [];

        queue.forEach(q => {
            let style = q.style;
            let feature = q.feature;
            let geometry = feature.geometry;
            let feature_labels = this.buildLabels(style.size, geometry, style);
            for (let i = 0; i < feature_labels.length; i++) {
                let label = feature_labels[i];
                let point_obj = {
                    feature,
                    draw: q.draw,
                    context: q.context,
                    style,
                    label
                };
                point_objs.push(point_obj);

                if (q.text_feature) {
                    let text_obj = {
                        feature,
                        draw: q.text_feature.draw,
                        context: q.context,
                        text: q.text_feature.text,
                        text_settings_key: q.text_feature.text_settings_key,
                        layout: q.text_feature.layout,
                        point_label: label,
                        linked: point_obj   // link so text only renders when parent point is placed
                    };
                    text_objs.push(text_obj);

                    // Unless text feature is optional, create a two-way link so that parent
                    // point will only render when text is also placed
                    if (!q.draw.text.optional) {
                        point_obj.linked = text_obj; // two-way link
                    }
                }
            }
        });

        // Collide both points and text, then build features
        const [, { labels, texts, textures }] = await Promise.all([
            // Points
            Collision.collide(point_objs, this.collision_group_points, tile.id).then(point_objs => {
                point_objs.forEach(q => {
                    this.feature_style = q.style;
                    this.feature_style.label = q.label;
                    this.feature_style.linked = q.linked; // TODO: move linked into label to avoid extra prop tracking?
                    Style.addFeature.call(this, q.feature, q.draw, q.context);
                });
            }),
            // Labels
            this.collideAndRenderTextLabels(tile, this.collision_group_text, text_objs)
        ]);

        // Process labels
        if (labels && texts) {
            // Build queued features
            labels.forEach(q => {
                let text_settings_key = q.text_settings_key;
                let text_info = texts[text_settings_key] && texts[text_settings_key][q.text];

                // setup styling object expected by Style class
                let style = this.feature_style;
                style.label = q.label;
                style.linked = q.linked; // TODO: move linked into label to avoid extra prop tracking?
                style.size = text_info.size.logical_size;
                style.texcoords = text_info.align[q.label.align].texcoords;
                style.label_texture = textures[text_info.align[q.label.align].texture_id];
                style.blend_order = q.draw.blend_order; // copy blend order from parent point

                Style.addFeature.call(this, q.feature, q.draw, q.context);
            });
        }
        this.freeText(tile);

        // Finish tile mesh
        const tile_data = await Style.endData.call(this, tile);
        // Attach tile-specific label atlas to mesh as a texture uniform
        if (tile_data && textures && textures.length) {
            tile_data.textures = tile_data.textures || [];
            tile_data.textures.push(...textures); // assign texture ownership to tile
        }
        return tile_data;
    },

    _preprocess (draw) {
        draw.color = StyleParser.createColorPropertyCache(draw.color);
        draw.alpha = StyleParser.createPropertyCache(draw.alpha);
        draw.texture = (draw.texture !== undefined ? draw.texture : this.texture); // optional or default texture
        draw.blend_order = this.getBlendOrderForDraw(draw); // from draw block, or fall back on default style blend order

        if (draw.outline) {
            draw.outline.color = StyleParser.createColorPropertyCache(draw.outline.color);
            draw.outline.alpha = StyleParser.createPropertyCache(draw.outline.alpha);
            draw.outline.width = StyleParser.createPropertyCache(draw.outline.width, StyleParser.parsePositiveNumber);
        }

        draw.z = StyleParser.createPropertyCache(draw.z, StyleParser.parseUnits);

        // Size (1d value or 2d array)
        try {
            draw.size = StyleParser.createPointSizePropertyCache(draw.size, draw.texture);
        }
        catch(e) {
            log({ level: 'warn', once: true }, `Layer group '${draw.layers.join(', ')}': ` +
                `${e} (${JSON.stringify(draw.size)}), features that match this layer group won't be drawn.`);
            return null;
        }

        // Offset (2d array)
        draw.offset = StyleParser.createPropertyCache(draw.offset,
            v => Array.isArray(v) && v.map(StyleParser.parseNumber)
        );

        // Buffer (1d value or or 2d array) - must be >= 0
        draw.buffer = StyleParser.createPropertyCache(draw.buffer,
            v => (Array.isArray(v) ? v : [v, v]).map(StyleParser.parsePositiveNumber)
        );

        // Repeat rules - no repeat limitation for points by default
        draw.repeat_distance = StyleParser.createPropertyCache(draw.repeat_distance, StyleParser.parseNumber);

        // Placement strategies
        draw.placement = PLACEMENT[draw.placement && draw.placement.toUpperCase()];
        if (draw.placement == null) {
            draw.placement = PLACEMENT.VERTEX;
        }

        draw.placement_spacing = draw.placement_spacing != null ? draw.placement_spacing : 80; // default spacing
        draw.placement_spacing = StyleParser.createPropertyCache(draw.placement_spacing, StyleParser.parsePositiveNumber);

        draw.placement_min_length_ratio = draw.placement_min_length_ratio != null ? draw.placement_min_length_ratio : 1;
        draw.placement_min_length_ratio = StyleParser.createPropertyCache(draw.placement_min_length_ratio, StyleParser.parsePositiveNumber);

        if (typeof draw.angle === 'number') {
            draw.angle = draw.angle * Math.PI / 180; // convert static value to radians
        }
        else if (typeof draw.angle === 'function') {
            // convert function return value to radians
            const angle_func = draw.angle;
            draw.angle = context => angle_func(context) * Math.PI / 180;
        }
        else {
            draw.angle = draw.angle || 0; // angle can be a string like "auto" (use angle of geometry)
        }

        // Optional text styling
        draw.text = this.preprocessText(draw.text); // will return null if valid text styling wasn't provided
        if (draw.text) {
            draw.text.key = draw.key; // inherits parent properties
            draw.text.group = draw.group;
            draw.text.layers = draw.layers;
            draw.text.order = draw.order;
            draw.text.blend_order = draw.blend_order;
            draw.text.repeat_group = (draw.text.repeat_group != null ? draw.text.repeat_group : draw.repeat_group);
            draw.text.anchor = draw.text.anchor || this.default_anchor;
            draw.text.optional = (typeof draw.text.optional === 'boolean') ? draw.text.optional : false; // default text to required
            draw.text.interactive = draw.text.interactive || draw.interactive; // inherits from point
        }

        return draw;
    },

    // Default to trying all anchor placements
    default_anchor: ['bottom', 'top', 'right', 'left'],

    // Compute label layout-related properties
    computeLayout (target, feature, draw, context, tile) {
        let layout = target || {};
        layout.id = feature;
        layout.units_per_pixel = tile.units_per_pixel || 1;

        // collision flag
        layout.collide = (draw.collide === false) ? false : true;

        // label anchors (point labels only)
        // label position will be adjusted in the given direction, relative to its original point
        // one of: left, right, top, bottom, top-left, top-right, bottom-left, bottom-right
        layout.anchor = draw.anchor;

        // label offset and buffer in pixel (applied in screen space)
        layout.offset = StyleParser.evalCachedProperty(draw.offset, context) || StyleParser.zeroPair;
        layout.buffer = StyleParser.evalCachedProperty(draw.buffer, context) || StyleParser.zeroPair;

        // repeat rules
        layout.repeat_distance = StyleParser.evalCachedProperty(draw.repeat_distance, context);
        if (layout.repeat_distance) {
            layout.repeat_distance *= layout.units_per_pixel;
            layout.repeat_scale = 1; // initial repeat pass in tile with full scale

            if (typeof draw.repeat_group === 'function') {
                layout.repeat_group = draw.repeat_group(context); // dynamic repeat group
            }
            else {
                // default to top-level layer name
                // (e.g. all labels under `roads` layer, including sub-layers, are in one repeat group)
                layout.repeat_group = draw.repeat_group || context.layer;
            }
        }

        // label priority (lower is higher)
        let priority = draw.priority;
        if (priority != null) {
            if (typeof priority === 'function') {
                priority = priority(context);
            }
        }
        else {
            priority = -1 >>> 0; // default to max priority value if none set
        }
        layout.priority = priority;

        return layout;
    },

    // Implements label building for TextLabels mixin
    buildTextLabels (tile, feature_queue) {
        let labels = [];
        for (let f=0; f < feature_queue.length; f++) {
            let fq = feature_queue[f];
            let text_info = this.texts[tile.id][fq.text_settings_key][fq.text];
            let size = text_info.size.collision_size;
            fq.label = new LabelPoint(fq.point_label.position, size, fq.layout);
            labels.push(fq);
        }
        return labels;
    },

    // Builds one or more point labels for a geometry
    buildLabels (size, geometry, layout) {
        let labels = [];

        if (geometry.type === 'Point') {
            labels.push(new LabelPoint(geometry.coordinates, size, layout, layout.angle));
        }
        else if (geometry.type === 'MultiPoint') {
            let points = geometry.coordinates;
            for (let i = 0; i < points.length; ++i) {
                let point = points[i];
                labels.push(new LabelPoint(point, size, layout, layout.angle));
            }
        }
        else if (geometry.type === 'LineString') {
            let line = geometry.coordinates;
            let point_labels = placePointsOnLine(line, size, layout);
            for (let i = 0; i < point_labels.length; ++i) {
                labels.push(point_labels[i]);
            }
        }
        else if (geometry.type === 'MultiLineString') {
            let lines = geometry.coordinates;
            for (let ln = 0; ln < lines.length; ln++) {
                let line = lines[ln];
                let point_labels = placePointsOnLine(line, size, layout);
                for (let i = 0; i < point_labels.length; ++i) {
                    labels.push(point_labels[i]);
                }
            }
        }
        else if (geometry.type === 'Polygon') {
            // Point at polygon centroid (of outer ring)
            if (layout.placement === PLACEMENT.CENTROID) {
                let centroid = Geo.centroid(geometry.coordinates);
                if (centroid) { // skip degenerate polygons
                    labels.push(new LabelPoint(centroid, size, layout, layout.angle));
                }
            }
            // Point at each polygon vertex (all rings)
            else {
                let rings = geometry.coordinates;
                for (let ln = 0; ln < rings.length; ln++) {
                    let point_labels = placePointsOnLine(rings[ln], size, layout);
                    for (let i = 0; i < point_labels.length; ++i) {
                        labels.push(point_labels[i]);
                    }
                }
            }
        }
        else if (geometry.type === 'MultiPolygon') {
            if (layout.placement === PLACEMENT.CENTROID) {
                let centroid = Geo.multiCentroid(geometry.coordinates);
                if (centroid) { // skip degenerate polygons
                    labels.push(new LabelPoint(centroid, size, layout, layout.angle));
                }
            }
            else {
                let polys = geometry.coordinates;
                for (let p = 0; p < polys.length; p++) {
                    let rings = polys[p];
                    for (let ln = 0; ln < rings.length; ln++) {
                        let point_labels = placePointsOnLine(rings[ln], size, layout);
                        for (let i = 0; i < point_labels.length; ++i) {
                            labels.push(point_labels[i]);
                        }
                    }
                }
            }
        }

        return labels;
    },

    /**
     * A "template" that sets constant attibutes for each vertex, which is then modified per vertex or per feature.
     * A plain JS array matching the order of the vertex layout.
     */
    makeVertexTemplate(style, mesh, add_custom_attribs = true) {
        let i = 0;

        // a_position.xyz - vertex position
        // a_position.w - layer order
        this.vertex_template[i++] = 0;
        this.vertex_template[i++] = 0;
        this.vertex_template[i++] = style.z || 0;
        this.vertex_template[i++] = this.scaleOrder(style.order);

        // a_shape.xy - size of point in pixels (scaling vector)
        // a_shape.z - angle of point
        // a_shape.w - show/hide flag
        this.vertex_template[i++] = 0;
        this.vertex_template[i++] = 0;
        this.vertex_template[i++] = 0;
        this.vertex_template[i++] = style.label.layout.collide ? 0 : 1; // set initial label hide/show state

        // a_texcoord.xy - texture coords
        if (!mesh.variant.shader_point) {
            this.vertex_template[i++] = 0;
            this.vertex_template[i++] = 0;
        }

        // a_offset.xy - offset of point from center, in pixels
        this.vertex_template[i++] = 0;
        this.vertex_template[i++] = 0;

        // a_color.rgba - feature color
        const color = style.color || StyleParser.defaults.color;
        this.vertex_template[i++] = color[0] * 255;
        this.vertex_template[i++] = color[1] * 255;
        this.vertex_template[i++] = color[2] * 255;
        this.vertex_template[i++] = (style.alpha != null ? style.alpha : color[3]) * 255;

        // a_selection_color.rgba - selection color
        if (mesh.variant.selection) {
            this.vertex_template[i++] = style.selection_color[0] * 255;
            this.vertex_template[i++] = style.selection_color[1] * 255;
            this.vertex_template[i++] = style.selection_color[2] * 255;
            this.vertex_template[i++] = style.selection_color[3] * 255;
        }

        // point outline
        if (mesh.variant.shader_point) {
            // a_outline_color.rgba - outline color
            const outline_color = style.outline_color || StyleParser.defaults.outline.color;
            this.vertex_template[i++] = outline_color[0] * 255;
            this.vertex_template[i++] = outline_color[1] * 255;
            this.vertex_template[i++] = outline_color[2] * 255;
            this.vertex_template[i++] = (style.outline_alpha != null ? style.outline_alpha : outline_color[3]) * 255;

            // a_outline_edge - point outline edge (as % of point size where outline begins)
            this.vertex_template[i++] = style.outline_edge_pct || StyleParser.defaults.outline.width;
        }

        if (add_custom_attribs) {
            this.addCustomAttributesToVertexTemplate(style, i);
        }
        return this.vertex_template;
    },

    buildQuad(point, size, angle, angles, pre_angles, offset, offsets, texcoords, curve, vertex_data, vertex_template) {
        if (size[0] <= 0 || size[1] <= 0) {
            return 0; // size must be positive
        }

        return buildQuadForPoint(
            point,
            vertex_data,
            vertex_template,
            vertex_data.vertex_layout.index,
            size,
            offset,
            offsets,
            pre_angles,
            angle * 4096, // angle values have a 12-bit fraction
            angles,
            texcoords,
            curve
        );
    },

    // Build quad for point sprite
    build (style, context) {
        let label = style.label;
        if (label.type === 'curved') {
            return this.buildCurvedLabel(label, style, context);
        }
        else {
            return this.buildStraightLabel(label, style, context);
        }
    },

    buildStraightLabel (label, style, context) {
        let mesh = this.getTileMesh(context.tile, this.meshVariantTypeForDraw(style));
        let vertex_template = this.makeVertexTemplate(style, mesh);

        let size, texcoords;
        if (label.type !== 'point') {
            size = style.size[label.type];
            texcoords = style.texcoords[label.type].texcoord;
        }
        else {
            size = style.size;
            texcoords = style.texcoords;
        }

        // setup style or label texture if applicable
        mesh.uniforms = mesh.uniforms || {};
        if (style.label_texture) {
            mesh.uniforms.u_texture = style.label_texture;
            mesh.uniforms.u_point_type = TANGRAM_POINT_TYPE_LABEL;
            mesh.uniforms.u_apply_color_blocks = false;
        }
        else if (style.texture) {
            mesh.uniforms.u_texture = style.texture;
            mesh.uniforms.u_point_type = TANGRAM_POINT_TYPE_TEXTURE;
            mesh.uniforms.u_apply_color_blocks = true;
        }
        else {
            mesh.uniforms.u_texture = Texture.default; // ensure a tetxure is always bound to avoid GL warnings ('no texture bound to unit' in Chrome)
            mesh.uniforms.u_point_type = TANGRAM_POINT_TYPE_SHADER;
            mesh.uniforms.u_apply_color_blocks = true;
        }

        let offset = label.offset;

        // TODO: instead of passing null, pass arrays with fingerprintable values
        // This value is checked in the shader to determine whether to apply curving logic
        let geom_count = this.buildQuad(
            label.position,                 // position
            size,                           // size in pixels
            label.angle,                    // angle in radians
            null,                           // placeholder for multiple angles
            null,                           // placeholder for multiple pre_angles
            offset,                         // offset from center in pixels
            null,                           // placeholder for multiple offsets
            texcoords,                      // texture UVs
            false,                          // if curved boolean
            mesh.vertex_data, vertex_template    // VBO and data for current vertex
        );

        // track label mesh buffer data
        const linked = (style.linked && style.linked.label.id);
        this.trackLabel(label, linked, mesh, geom_count, context);

        return geom_count;
    },

    buildCurvedLabel (label, style, context) {
        let mesh, vertex_template;
        let geom_count = 0;

        // two passes for stroke and fill, where stroke needs to be drawn first (painter's algorithm)
        // this ensures strokes don't overlap on other fills

        // pass for stroke
        for (let i = 0; i < label.num_segments; i++){
            let size = style.size[label.type][i];
            let texcoord_stroke = style.texcoords_stroke[i];

            // re-point to correct label texture
            style.label_texture = style.label_textures[i];
            mesh = this.getTileMesh(context.tile, this.meshVariantTypeForDraw(style));
            vertex_template = this.makeVertexTemplate(style, mesh);

            // add label texture uniform if needed
            mesh.uniforms = mesh.uniforms || {};
            mesh.uniforms.u_texture = style.label_texture;
            mesh.uniforms.u_point_type = TANGRAM_POINT_TYPE_LABEL;
            mesh.uniforms.u_apply_color_blocks = false;

            let offset = label.offset || [0,0];
            let position = label.position;

            let angles = label.angles[i];
            let offsets = label.offsets[i];
            let pre_angles = label.pre_angles[i];

            let seg_count = this.buildQuad(
                position,                       // position
                size,                           // size in pixels
                label.angle,                    // angle in degrees
                angles,                         // angles per segment
                pre_angles,                     // pre_angle array (rotation applied before offseting)
                offset,                         // offset from center in pixels
                offsets,                        // offsets per segment
                texcoord_stroke,                // texture UVs for stroked text
                true,                           // if curved
                mesh.vertex_data, vertex_template    // VBO and data for current vertex
            );
            geom_count += seg_count;

            // track label mesh buffer data
            const linked = (style.linked && style.linked.label.id);
            this.trackLabel(label, linked, mesh, seg_count, context);
        }

        // pass for fill
        for (let i = 0; i < label.num_segments; i++){
            let size = style.size[label.type][i];
            let texcoord = style.texcoords[label.type][i].texcoord;

            // re-point to correct label texture
            style.label_texture = style.label_textures[i];
            mesh = this.getTileMesh(context.tile, this.meshVariantTypeForDraw(style));
            vertex_template = this.makeVertexTemplate(style, mesh);

            // add label texture uniform if needed
            mesh.uniforms = mesh.uniforms || {};
            mesh.uniforms.u_texture = style.label_texture;
            mesh.uniforms.u_point_type = TANGRAM_POINT_TYPE_LABEL;
            mesh.uniforms.u_apply_color_blocks = false;

            let offset = label.offset || [0,0];
            let position = label.position;

            let angles = label.angles[i];
            let offsets = label.offsets[i];
            let pre_angles = label.pre_angles[i];

            let seg_count = this.buildQuad(
                position,                       // position
                size,                           // size in pixels
                label.angle,                    // angle in degrees
                angles,                         // angles per segment
                pre_angles,                     // pre_angle array (rotation applied before offseting)
                offset,                         // offset from center in pixels
                offsets,                        // offsets per segment
                texcoord,                       // texture UVs for fill text
                true,                           // if curved
                mesh.vertex_data, vertex_template    // VBO and data for current vertex
            );
            geom_count += seg_count;

            // track label mesh buffer data
            const linked = (style.linked && style.linked.label.id);
            this.trackLabel(label, linked, mesh, seg_count, context);
        }

        return geom_count;
    },

    // track mesh data for label on main thread, for additional cross-tile collision/repeat passes
    trackLabel (label, linked, mesh, geom_count/*, context*/) {
        // track if collision is enabled, or if the label is near enough to the tile edge to
        // necessitate further repeat checking
        if (label.layout.collide || label.may_repeat_across_tiles) {
            mesh.labels = mesh.labels || {};
            mesh.labels[label.id] = mesh.labels[label.id] || {
                container: {
                    label: label.toJSON(),
                    linked,
                },
                ranges: [],
                // debug: { // uncomment and pass in context for debugging
                //     id: context.feature.properties.id,
                //     name: context.feature.properties.name,
                //     props: JSON.stringify(context.feature.properties),
                //     point_type: mesh.uniforms.u_point_type
                // }
            };

            // store byte ranges occupied by label in VBO, so they can be updated on main thread
            const vertex_count = geom_count * 2; // geom count is triangles: 2 triangles = 1 quad = 4 vertices
            const start = mesh.vertex_data.offset - mesh.vertex_data.stride * vertex_count; // start offset of byte range
            mesh.labels[label.id].ranges.push([
                start,
                vertex_count
            ]);
        }
    },

    // Override to pass-through to generic point builder
    buildLines (lines, style, context) {
        return this.build(style, context);
    },

    buildPoints (points, style, context) {
        return this.build(style, context);
    },

    buildPolygons (points, style, context) {
        return this.build(style, context);
    },

    // Override
    // Create or return desired vertex layout permutation based on flags
    vertexLayoutForMeshVariant (variant) {
        // Vertex layout only depends on shader point flag, so using it as layout key to avoid duplicate layouts
        if (this.vertex_layouts[variant.shader_point] == null) {
            // Attributes for this mesh variant
            // Optional attributes have placeholder values assigned with `static` parameter
            // TODO: could support optional attributes for selection and offset, but may not be worth it
            // since points generally don't consume much memory anyway
            const attribs = [
                { name: 'a_position', size: 4, type: gl.SHORT, normalized: false },
                { name: 'a_shape', size: 4, type: gl.SHORT, normalized: false },
                { name: 'a_texcoord', size: 2, type: gl.UNSIGNED_SHORT, normalized: true, static: (variant.shader_point ? [0, 0] : null) },
                { name: 'a_offset', size: 2, type: gl.SHORT, normalized: false },
                { name: 'a_color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true },
                { name: 'a_selection_color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true, static: (variant.selection ? null : [0, 0, 0, 0]) },
                { name: 'a_outline_color', size: 4, type: gl.UNSIGNED_BYTE, normalized: true, static: (variant.shader_point ? null : [0, 0, 0, 0]) },
                { name: 'a_outline_edge', size: 1, type: gl.FLOAT, normalized: false, static: (variant.shader_point ? null : 0) }
            ];

            this.addCustomAttributesToAttributeList(attribs);
            this.vertex_layouts[variant.shader_point] = new VertexLayout(attribs);
        }
        return this.vertex_layouts[variant.shader_point];

    },

    // Override
    meshVariantTypeForDraw (draw) {
        const texture = draw.label_texture || draw.texture || SHADER_POINT_VARIANT; // unique key by texture name
        const key = texture + '/' + draw.blend_order;
        if (this.variants[key] == null) {
            this.variants[key] = {
                key,
                selection: 1, // TODO: make this vary by draw params
                shader_point: (texture === SHADER_POINT_VARIANT), // is shader point
                blend_order: draw.blend_order,
                mesh_order: (draw.label_texture ? 1 : 0) // put text on top of points (e.g. for highway shields, etc.)
            };
        }
        return this.variants[key]; // return pre-calculated mesh variant
    },

    // Override
    makeMesh (vertex_data, vertex_elements, options = {}) {
        // Add label fade time
        options = Object.assign({}, options, { fade_in_time: this.fade_in_time });
        return Style.makeMesh.call(this, vertex_data, vertex_elements, options);
    }

});
